<?php

namespace common\models;


use common\behaviors\FileUsageBehavior;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
use Yii;
use yii\db\ActiveRecord;
use yii\helpers\Html;
use yii\helpers\HtmlPurifier;
use yii\helpers\Url;

/**
 * Class File
 *
 * @property integer $id
 * @property integer $user_id
 * @property string $uri
 * @property string $file_name
 * @property string $file_size
 * @property string $file_mime
 * @property string $file_meta
 * @property string $bundle
 * @property string $name
 * @property integer $status
 * @property integer $updated_at
 * @property integer $created_at
 * @property User $user
 *
 * @package common\models
 */
class File extends ActiveRecord
{
    const IMAGE_ORIGIN = 'origin';
    const IMAGE_THUMBNAIL = 'thumbnail';
    const IMAGE_MEDIUM = 'medium';
    const IMAGE_LARGE = 'large';
    const IMAGE_POST_THUMBNAIL = 'post-thumbnail';

    /**
     * 未使用的
     *
     * @var integer
     */
    const STATUS_TEMPORARY = 0;

    /**
     * 被使用了的
     *
     * @var integer
     */
    const STATUS_PERMANENT = 1;

    public $file;

    /**
     * @var array
     */
    private $_meta = [];

    /**
     * @inheritDoc
     */
    public function rules()
    {
        return [
            ['name', 'required'],
            [['name', 'alt'], 'string', 'max' => 128],
            [['caption', 'description'], 'safe'],
        ];
    }

    /**
     * @inheritDoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'file_name' => '名称',
            'file_size' => '大小',
            'file' => '文件',
            'user' => '上传者',
            'name' => '名称',
            'path' => '路径',
            'url' => 'URL',
            'alt' => '替换文本',
            'caption' => '说明',
            'description' => '描述',
            'updated_at' => '更新时间',
            'created_at' => '上传时间',
        ];
    }

    /**
     * @return array
     */
    public function behaviors()
    {
        if ($this->getScenario() === 'upload') {
            return [
                'fileUsage' => [
                    'class' => FileUsageBehavior::className(),
                    'fields' => [
                        'file' => [
                            'location' => 'public://files',
                            'many' => true,
                            'resize' => true,
                        ]
                    ],
                ],
            ];
        } else {
            return [];
        }
    }

    /**
     * @return array
     */
    public function getMeta()
    {
        return $this->_meta;
    }

    /**
     * @param array $meta
     */
    public function setMeta(array $meta)
    {
        $this->_meta = $meta;
    }

    /**
     * @param $name
     * @param $value
     */
    public function updateMeta($name, $value)
    {
        $this->_meta[$name] = $value;
    }

    /**
     * 是否是图片
     *
     * @return bool
     */
    public function isImage()
    {
        return !!preg_match('/jpe?g|gif|png$/i', $this->uri);
    }

    /**
     * 检测文件是否有相关的图片尺寸
     *
     * @param string $name
     * @return bool
     */
    public function hasImage($name)
    {
        return isset($this->_meta['sizes'][$name]);
    }

    /**
     * 获取图片URL 根据图片尺寸名
     *
     * @param string $name
     * @param boolean $force 如果图片尺寸不存在 是否强制返回图片
     * @return NULL|string
     */
    public function getImageUrl($name, $force = true)
    {
        if (!$this->hasImage($name)) {
            if ($name == self::IMAGE_THUMBNAIL || $force) {
                $name = self::IMAGE_ORIGIN;
            } else {
                return null;
            }
        }

        if ($name == self::IMAGE_ORIGIN) {
            return $this->createUrl($this->uri);
        } else {
            return $this->createUrl($this->_meta['sizes'][$name]['uri']);
        }
    }

    /**
     * 获取文件物理路径
     *
     * @return string
     */
    public function getPath()
    {
        return realpath(self::localPath($this->uri));
    }

    /**
     * 获取文件url
     *
     * @return string
     */
    public function getUrl()
    {
        return self::createUrl($this->uri);
    }

    /**
     * 获取文件名称
     *
     * @return string
     */
    public function getName()
    {
        if ($this->getIsNewRecord()) {
            return $this->file_name;
        }

        $ext = $this->getExtension();
        if (($pos = strrpos($this->file_name, $ext)) !== false) {
            return substr($this->file_name, 0, $pos - 1);
        } else {
            return $this->file_name;
        }
    }

    /**
     * 设置文件名称
     *
     * @param string $value
     */
    public function setName($value)
    {
        if ($this->getIsNewRecord()) {
            $this->file_name = $value;
        } else {
            if (trim($value) == '') {
                $this->file_name = '';
            } else {
                $this->file_name = $value . '.' . $this->getExtension();
            }
        }
    }

    /**
     * @return string 文件后缀名
     */
    public function getExtension()
    {
        return strtolower(pathinfo($this->file_name, PATHINFO_EXTENSION));
    }

    /**
     * 获取图片替代文本
     *
     * @return string
     */
    public function getAlt()
    {
        return isset($this->_meta['alt']) ? $this->_meta['alt'] : null;
    }

    /**
     * 设置图片替代文本
     *
     * @param $value
     */
    public function setAlt($value)
    {
        $this->_meta['alt'] = $value;
    }

    /**
     * 获取文件说明
     *
     * @return string
     */
    public function getCaption()
    {
        return isset($this->_meta['caption']) ? $this->_meta['caption'] : null;
    }

    /**
     * 设置文件说明
     *
     * @param string $value
     */
    public function setCaption($value)
    {
        $this->_meta['caption'] = $value;
    }

    /**
     * 获取文件名称
     *
     * @return string
     */
    public function getDescription()
    {
        return isset($this->_meta['description']) ? $this->_meta['description'] : null;
    }

    /**
     * 设置文件描述
     *
     * @param string $value
     */
    public function setDescription($value)
    {
        $this->_meta['description'] = $value;
    }

    /**
     * 获取类型
     *
     * @return string
     */
    public function getType()
    {
        $types = explode('/', $this->file_mime);
        return isset($types[0]) ? $types[0] : 'text';
    }

    /**
     * 获取子类型
     *
     * @return string
     */
    public function getSubType()
    {
        $types = explode('/', $this->file_mime);
        return isset($types[1]) ? $types[1] : 'other';
    }

    /**
     * 获取宽度
     *
     * @return integer|bool
     */
    public function getWidth()
    {
        $dimensions = $this->getDimensions();
        return isset($dimensions['width']) ? $dimensions['width'] : false;
    }

    /**
     * 获取高度
     *
     * @return integer|bool
     */
    public function getHeight()
    {
        $dimensions = $this->getDimensions();
        return isset($dimensions['height']) ? $dimensions['height'] : false;
    }

    /**
     * 获取图片尺寸
     *
     * @return bool|array
     */
    public function getDimensions()
    {
        if (isset($this->_meta['sizes'][File::IMAGE_ORIGIN])) {
            return $this->_meta['sizes'][File::IMAGE_ORIGIN];
        } elseif (isset($this->_meta['sizes'][File::IMAGE_THUMBNAIL])) {
            return $this->_meta['sizes'][File::IMAGE_THUMBNAIL];
        } else {
            return false;
        }
    }

    /**
     * 获取文件的图标
     *
     * @param array $options
     * @return string
     */
    public function getIcon($options = [])
    {
        if ($this->isImage() && $this->uriScheme($this->uri) === 'public') {
            if (!$this->hasImage('thumbnail')) {
                $options['style'] = 'max-width: 100%;';
            }
            return $this->getThumbnail(array_merge(['width' => 60, 'height' => 60], $options));
        } elseif (Yii::$app->has('crystal')) {
            return Yii::$app->get('crystal')->getIcon($this->getExtension(), Html::encode($this->getAlt()), $options);
        } else {
            return '';
        }
    }

    /**
     * 获取文件的缩略图
     *
     * @param array $htmlOptions
     * @return string
     */
    public function getThumbnail($htmlOptions = [])
    {
        return $this->getImage(self::IMAGE_THUMBNAIL, $htmlOptions);
    }

    /**
     * 获取图片 根据生成的图片尺寸名
     *
     * @param string $name 图片尺寸名
     * @param array $options
     * @return string
     */
    public function getImage($name = self::IMAGE_POST_THUMBNAIL, $options = [])
    {
        if (!$this->hasImage($name)) {
            $name = self::IMAGE_ORIGIN;
        }

        if ($name == self::IMAGE_ORIGIN) {
            $meta = isset($this->_meta['sizes'][self::IMAGE_ORIGIN]) ? $this->_meta['sizes'][self::IMAGE_ORIGIN] : array();
            $src = $this->createUrl($this->uri);
        } else {
            $meta = isset($this->_meta['sizes'][$name]) ? $this->_meta['sizes'][$name] : [];;
            $src = $this->createUrl($meta['uri']);
        }

        if ($meta) {
            $options = array_merge([
                'width' => $meta['width'],
                'height' => $meta['height'],
            ], $options);
        }

        if (empty($options['width'])) {
            unset($options['width']);
        }

        if (empty($options['height'])) {
            unset($options['height']);
        }

        if (!isset($options['alt'])) {
            $options['alt'] = $this->getAlt() ? $this->getAlt() : $this->name;
        }

        return Html::img($src, $options);
    }

    /**
     * 获取尺寸
     *
     * @return array
     */
    public function getImageSizes()
    {
        $data = [];
        if (isset($this->_meta['sizes'])) {
            foreach ($this->_meta['sizes'] as $name => $size) {
                $data[$name]['width'] = $size['width'];
                $data[$name]['height'] = $size['height'];
                if ($name == self::IMAGE_ORIGIN) {
                    $data[$name]['url'] = $this->getUrl();
                } else {
                    $data[$name]['url'] = $this->createUrl($size['uri']);
                }

                if ($data[$name]['width'] > $data[$name]['height']) {
                    $data[$name]['orientation'] = 'landscape';
                } else {
                    $data[$name]['orientation'] = 'portrait';
                }
            }
        }
        return $data;
    }

    /**
     * @param $name
     * @return array|null
     */
    public function getImageSizeByName($name)
    {
        $imageSizes = $this->getImageSizes();
        return isset($imageSizes[$name]) ? $imageSizes[$name] : null;
    }

    /**
     * 获取文件的图标地址
     *
     * @return string|NULL
     */
    public function getIconUrl()
    {
        if ($this->isImage() && $this->uriScheme($this->uri) === 'public') {
            $iconUrl = $this->getImageUrl(self::IMAGE_THUMBNAIL);
        }

        if (!isset($iconUrl) && Yii::$app->has('crystal')) {
            $iconUrl = Yii::$app->get('crystal')->getIconUrl($this->getExtension());
        }

        return isset($iconUrl) ? $iconUrl : null;
    }

    /**
     * @return array
     */
    public function toAttachment()
    {
        $attachment = [
            'id' => $this->id,
            'title' => Html::encode($this->name),
            'filename' => Html::encode($this->file_name),
            'url' => $this->getUrl(),
            'alt' => Html::encode($this->getAlt()),
            'userId' => $this->user_id,
            'description' => HTMLPurifier::process($this->getDescription()),
            'caption' => Html::encode($this->getCaption()),
            'uploadedAt' => $this->created_at * 1000,
            'modifiedAt' => $this->created_at * 1000,
            'mime' => $this->file_mime,
            'type' => $this->getType(),
            'subtype' => $this->getSubType(),
            'iconUrl' => $this->getIconUrl(),
            'fileSize' => Yii::$app->formatter->asShortSize($this->file_size),
            'dateFormatted' => Yii::$app->formatter->asDatetime($this->created_at),
            'editLink' => Url::to(['/file/update', 'id' => $this->id], true),
            'deleteLink' => Url::to(['/file/delete', 'id' => $this->id], true),
        ];

        if ($attachment['type'] === 'image') {
            $attachment['width'] = $this->getWidth();
            $attachment['height'] = $this->getHeight();
            $attachment['imageSizes'] = $this->getImageSizes();
            $attachment['orientation'] = $attachment['width'] > $attachment['height'] ? 'landscape' : 'portrait';
        }

        return $attachment;
    }

    /**
     * 删除缓存
     */
    public function deleteCache()
    {
        if (!empty(Yii::$app->params['file.cache_enabled'])) {
            Yii::$app->getCache()->delete(self::getCacheKey($this->id));
        }
    }

    /**
     * 删除文件
     */
    public function deleteFile()
    {
        $sizes = isset($this->_meta['sizes']) ? $this->_meta['sizes'] : [];
        $backupSizes = isset($this->_meta['_backup_sizes']) ? $this->_meta['_backup_sizes'] : array();
        $sizes = array_merge($sizes, $backupSizes);
        foreach ($sizes as $name => $meta) {
            if ($name == File::IMAGE_ORIGIN) {
                continue;
            }

            if (isset($meta['uri']) && ($filePath = realpath(self::localPath($meta['uri'])))) {
                if (!@unlink($filePath)) {
                    Yii::warning(sprintf('删除文件 %s 失败', $filePath));
                }
            }
        }

        $filePath = $this->getPath();
        if ($filePath) {
            if (!@unlink($filePath)) {
                Yii::warning(sprintf('删除文件 %s 失败', $filePath));
            }
        }
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getUser()
    {
        return $this->hasOne(User::className(), ['id' => 'user_id']);
    }

    /**
     * @inheritDoc
     */
    public function afterFind()
    {
        parent::afterFind();
        $this->_meta = unserialize($this->file_meta);
    }

    /**
     * @inheritDoc
     */
    public function beforeDelete()
    {
        if (parent::beforeDelete()) {
            if ($this->bundle != get_class($this) && $this->status == self::STATUS_PERMANENT) {
                return false;
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * @inheritDoc
     */
    public function beforeSave($insert)
    {
        if (parent::beforeSave($insert)) {
            if ($this->isNewRecord) {
                $this->created_at = time();
            }
            $this->updated_at = time();
            $this->file_meta = serialize($this->_meta);
            return true;
        } else {
            return false;
        }
    }

    /**
     * @inheritDoc
     */
    public function afterSave($insert, $changedAttributes)
    {
        parent::afterSave($insert, $changedAttributes);
        if (!empty(Yii::$app->params['file.cache_enabled'])) {
            Yii::$app->getCache()->set(self::getCacheKey($this->id), $this->getAttributes(),
                isset(Yii::$app->params['file.cache_expire']) ?
                    Yii::$app->params['file.cache_expire'] : 2592000);
        }
    }

    /**
     * @inheritDoc
     */
    public function afterDelete()
    {
        parent::afterDelete();
        $this->deleteFile();
        $this->deleteCache();
    }

    /**
     * @param $ids
     * @throws \yii\base\InvalidConfigException
     * @return File[]|File
     */
    public static function findFromCache($ids)
    {
        if (empty(Yii::$app->params['file.cache_enabled'])) {
            throw new InvalidConfigException('文件缓存未开启');
        }

        $models = array();
        $unCached = array();

        foreach ((array)$ids as $id) {
            $cacheKey = self::getCacheKey($id);
            if (($data = Yii::$app->getCache()->get($cacheKey)) !== false) {
                $model = self::instantiate($data);
                self::populateRecord(self::instantiate($data), $data);
                $model->afterFind();
                $models[] = $model;
            } else {
                $unCached[] = $id;
            }
        }

        if ($unCached) {
            /** @var File $model */
            foreach (self::findAll(['id' => $unCached]) as $model) {
                $cacheKey = self::getCacheKey($model->id);
                Yii::$app->getCache()->add($cacheKey, $model->getAttributes(),
                    isset(Yii::$app->params['file.cache_expire']) ?
                        Yii::$app->params['file.cache_expire'] : 2592000);
                $models[] = $model;
            }
        }

        return is_array($ids) ? $models : (isset($models[0]) ? $models[0] : null);
    }

    /**
     * 获取缓存 key
     * @param $id
     * @return string
     */
    public static function getCacheKey($id)
    {
        return 'file_' . $id;
    }

    /**
     * 获取默认的允许上传的文件后缀
     *
     * @return string
     */
    public static function fileExtensions()
    {
        return Yii::$app->get('option')->get('file.extensions', 'gif,png,jpg,jpeg,doc,pdf');
    }

    /**
     * 获取默认的允许上传的文件MIME
     *
     * @return string
     */
    public static function fileMimeTypes()
    {
        return Yii::$app->get('option')->get('file.mime_types', '');
    }

    /**
     * php环境 允许的上传大小
     *
     * @return string
     */
    public static function phpFileMaxSize()
    {
        return ((int)($maxUp = @ini_get('upload_max_filesize')) < (int)($maxPost = @ini_get('post_max_size'))) ? $maxUp : $maxPost;
    }

    /**
     * 获取默认的上传文件的最大大小
     *
     * @return integer
     */
    public static function fileMaxSize()
    {
        if (Yii::$app->get('option')->get('file.max_size')) {
            return min(self::sizeToBytes(self::phpFileMaxSize()), self::sizeToBytes(Yii::$app->get('option')->get('file.max_size')));
        } else {
            return self::sizeToBytes(self::phpFileMaxSize());
        }
    }

    /**
     * 上传文件默认图片尺寸列表
     *
     * @return array
     */
    public static function imageSizes()
    {
        return Yii::$app->get('option')->get('file.image_sizes', array(
            self::IMAGE_THUMBNAIL => [150, 150, true],
        ));
    }

    /**
     * @return array
     */
    public static function intermediateImageSizes()
    {
        return array_keys(self::imageSizes());
    }

    /**
     * @param $currentWidth
     * @param $currentHeight
     * @param int $maxWidth
     * @param int $maxHeight
     * @return array
     */
    public static function constrainDimensions($currentWidth, $currentHeight, $maxWidth = 0, $maxHeight = 0)
    {
        if (!$maxWidth and !$maxHeight)
            return array($currentWidth, $currentHeight);

        $widthRatio = $heightRatio = 1.0;
        $didWidth = $didHeight = false;

        if ($maxWidth > 0 && $currentWidth > 0 && $currentWidth > $maxWidth) {
            $widthRatio = $maxWidth / $currentWidth;
            $didWidth = true;
        }

        if ($maxHeight > 0 && $currentHeight > 0 && $currentHeight > $maxHeight) {
            $heightRatio = $maxHeight / $currentHeight;
            $didHeight = true;
        }

        // Calculate the larger/smaller ratios
        $smallerRatio = min($widthRatio, $heightRatio);
        $largerRatio = max($widthRatio, $heightRatio);

        if (intval($currentWidth * $largerRatio) > $maxWidth || intval($currentHeight * $largerRatio) > $maxHeight)
            // The larger ratio is too big. It would result in an overflow.
            $ratio = $smallerRatio;
        else
            // The larger ratio fits, and is likely to be a more "snug" fit.
            $ratio = $largerRatio;

        // Very small dimensions may result in 0, 1 should be the minimum.
        $w = max(1, intval($currentWidth * $ratio));
        $h = max(1, intval($currentHeight * $ratio));

        // Sometimes, due to rounding, we'll end up with a result like this: 465x700 in a 177x177 box is 117x176... a pixel short
        // We also have issues with recursive calls resulting in an ever-changing result. Constraining to the result of a constraint should yield the original result.
        // Thus we look for dimensions that are one pixel shy of the max value and bump them up
        if ($didWidth && $w == $maxWidth - 1)
            $w = $maxWidth; // Round it up
        if ($didHeight && $h == $maxHeight - 1)
            $h = $maxHeight; // Round it up

        return array($w, $h);
    }

    /**
     * 把大小转换成字节
     *
     * @param string $sizeStr
     * @return integer
     */
    public static function sizeToBytes($sizeStr)
    {
        // get the latest character
        switch (strtolower(substr($sizeStr, -1))) {
            case 'm':
                return (int)$sizeStr * 1048576; // 1024 * 1024
            case 'k':
                return (int)$sizeStr * 1024; // 1024
            case 'g':
                return (int)$sizeStr * 1073741824; // 1024 * 1024 * 1024
            default:
                return (int)$sizeStr; // do nothing
        }
    }

    /**
     * 根据$uri转换本地路径
     *
     * @param string $uri
     * @return boolean|string
     */
    public static function localPath($uri)
    {
        static $cache = array();

        $directoryPath = self::directoryPath($uri);
        if ($directoryPath === false) {
            return false;
        }

        if ($directoryPath[0] === '@') {
            $directoryPath = Yii::getAlias($directoryPath);
        }

        if (!isset($cache[$directoryPath])) {
            $cache[$directoryPath] = realpath($directoryPath);
        }

        if ($cache[$directoryPath] === false) {
            return false;
        }

        $path = $cache[$directoryPath] . '/' . self::uriTarget($uri);
        return $path;
    }

    /**
     * 根据$uri获取协议下的目录路径
     *
     * @param string $uri
     * @return boolean|string
     */
    public static function directoryPath($uri)
    {
        $scheme = self::uriScheme($uri);
        $schemeDirectoryMap = self::schemeDirectoryMap();
        return isset($schemeDirectoryMap[$scheme]) ? $schemeDirectoryMap[$scheme] : false;
    }

    /**
     * 获取$uri中的协议
     *
     * @param string $uri
     * @return boolean|string
     */
    public static function uriScheme($uri)
    {
        $pos = strpos($uri, '://');
        return $pos ? substr($uri, 0, $pos) : false;
    }

    /**
     * 协议目录映射
     *
     * @return array
     */
    public static function schemeDirectoryMap()
    {
        return array(
            'public' => Yii::$app->get('option')->get('file.public_path', '@uploads'),
        );
    }

    public static function schemeUrlMap()
    {
        return array(
            'public' => '127.0.0.1:99'
        );
    }

    /**
     * 根据$uri获取其目标目录
     *
     * @param string $uri
     * @return boolean|string
     */
    public static function uriTarget($uri)
    {
        $data = explode('://', $uri, 2);
        return count($data) == 2 ? trim($data[1], '\/') : false;
    }

    /**
     * 根据$uri获取url
     *
     * @param string $uri
     * @throws \yii\base\InvalidParamException
     * @return string
     */
    public static function createUrl($uri)
    {
        $request = Yii::$app->getRequest();
        $schemeUrlMap = self::schemeUrlMap();
        $scheme = self::uriScheme($uri);
        if (!isset($schemeUrlMap[$scheme])) {
            $absoluteUrl = $request->getHostInfo() . $request->getBaseUrl();
        } else {
            $absoluteUrl = $schemeUrlMap[$scheme];
        }

        if (!$scheme) {
            if (substr($uri, 0, 1) == '/') {
                return $uri;
            } else {
                return $absoluteUrl . '/' . str_replace('%2F', '/', rawurlencode($uri));
            }
        } elseif ($scheme == 'http' || $scheme == 'https') {
            return $uri;
        } else {
            $schemeDirectoryMap = self::schemeDirectoryMap();
            if (!isset($schemeDirectoryMap[$scheme])) {
                throw new InvalidParamException(sprintf('%s 协议没有映射路径', $scheme));
            }
            $directoryPath = $schemeDirectoryMap[$scheme];
            if ($directoryPath[0] === '@') {
                $directoryPath = substr($directoryPath, 1);
            }
            if (!isset($schemeUrlMap[$scheme])) {
                return $absoluteUrl . '/' . $directoryPath . '/' . self::uriTarget($uri);
            } else {
               $url = $schemeUrlMap[$scheme] . '/' . self::uriTarget($uri);
               if (!preg_match('#^https?:\/\/#', $url)) {
                    return ($request->isSecureConnection ? 'https://'  : 'http://')  . $url;
               } else {
                   return $url;
               }
            }
        }
    }
}